﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SR = System.Reflection;
using System.IO;
using Mono.Cecil;
using Mono.Cecil.Cil;

namespace Hack.Compilers.BrainFuck
{
    public static class Compiler
    {
        private static AssemblyDefinition assembly;
        private static TypeDefinition type;
        private static MethodDefinition readMethod;
        private static MethodDefinition queueArgumentsMethod;

        private static TypeReference Import(Type t)
        {
            return assembly.MainModule.Import(t);
        }
        private static MethodReference Import(SR.MethodInfo mi)
        {
            return assembly.MainModule.Import(mi);
        }

        private static AssemblyDefinition CompileNormal(string name, string source, bool ignore, bool endOfArgs)
        {
            MethodDefinition method = new MethodDefinition("main",
                MethodAttributes.Public | MethodAttributes.Static, Import(typeof(void)));
            method.Parameters.Add(new ParameterDefinition("args", 0, (ParameterAttributes)0, Import(typeof(string[]))));
            GenerateCIL(method, source, ignore, endOfArgs);
            type.Methods.Add(method);

            assembly.EntryPoint = method;
            return assembly;
        }

        private static AssemblyDefinition CompileInterpreter(string name, string source, bool endOfArgs)
        {
            System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
            byte[] data = encoding.GetBytes(source);

            EmbeddedResource res = new EmbeddedResource("source", (ManifestResourceAttributes)0, data);
            assembly.MainModule.Resources.Add(res);

            Import(typeof(System.Reflection.Assembly));

            MethodDefinition interpretMainMethod = null;

            string path = SR.Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName;
            AssemblyDefinition thisAssembly = AssemblyFactory.GetAssembly(path);
            foreach (TypeDefinition t in thisAssembly.MainModule.Types)
            {
                if (t.Name == "InjectedInterpretMethods")
                {
                    TypeDefinition injectedType = type.Module.Inject(t);
                    interpretMainMethod = injectedType.Methods.GetMethod("InterpretMain")[0];
                    type.Module.Types.Add(injectedType);
                }
                else if (t.Name == "InterpreterReader" || t.Name == "Interpreter")
                {
                    type.Module.Types.Add(type.Module.Inject(t));
                }
            }

            MethodDefinition method = new MethodDefinition("main",
                MethodAttributes.Public | MethodAttributes.Static, Import(typeof(void)));
            method.Parameters.Add(new ParameterDefinition("args", 0, (ParameterAttributes)0, Import(typeof(string[]))));

            method.Body.CilWorker.Emit(OpCodes.Ldarg_0);
            method.Body.CilWorker.Emit(endOfArgs ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
            method.Body.CilWorker.Emit(OpCodes.Call, interpretMainMethod);
            method.Body.CilWorker.Emit(OpCodes.Ret);
            type.Methods.Add(method);

            assembly.EntryPoint = method;

            return assembly;
        }

        public static AssemblyDefinition Compile(string name, string source, bool ignore, bool intArray, bool endOfArgs, bool embed)
        {
            Compiler.intArray = intArray;
            assembly = AssemblyFactory.DefineAssembly(name, name + ".exe", TargetRuntime.NET_2_0, AssemblyKind.Console);

            type = new TypeDefinition("Program", "brainfucker",
                TypeAttributes.AutoClass | TypeAttributes.Public, Import(typeof(object)));
            assembly.MainModule.Types.Add(type);

            Import(typeof(int));
            Import(typeof(System.Collections.Generic.Queue<int>));
            Import(typeof(System.IO.File));

            string path = SR.Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName;
            AssemblyDefinition thisAssembly = AssemblyFactory.GetAssembly(path);
            foreach (TypeDefinition t in thisAssembly.MainModule.Types)
            {
                if (t.Name == "InjectedMethods")
                {
                    TypeDefinition injectedType = type.Module.Inject(t);

                    readMethod = injectedType.Methods.GetMethod("Read")[0];
                    queueArgumentsMethod = injectedType.Methods.GetMethod("QueueArguments")[0];

                    type.Module.Types.Add(injectedType);
                    break;
                }
            }
            return embed ? CompileInterpreter(name, source, endOfArgs) :
                CompileNormal(name, source, ignore, endOfArgs);

        }

        private static CilWorker worker;
        private static VariableDefinition array;
        private static VariableDefinition index;
        private static VariableDefinition args;

        private class Loop
        {
            public Instruction LoopStart;
            public Instruction ConditionStart;
        }

        private static int array_size = 1;
        private static int cell = 1;
        private static int array_start_offset = 0;
        private static int loop = 0;
        private static List<Loop> loopList = new List<Loop>();

        private static int crementValue;
        private static bool crementing;
        private static int moveValue;
        private static bool moving;

        private static char beginComment = ' ';
        private static bool endMultilineComment;
        private static bool inMultilineComment;
        private static bool inComment;

        private static bool intArray;

        private static void GenerateCIL(MethodDefinition md, string source, bool ignore, bool endOfArgs)
        {
            worker = md.Body.CilWorker;

            array = new VariableDefinition("array", 0, md, Import(intArray ? typeof(int[]) : typeof(byte[])));
            index = new VariableDefinition("index", 1, md, Import(typeof(int)));
            args = new VariableDefinition("args", 2, md, Import(intArray ?
                typeof(System.Collections.Generic.Queue<int>) :
                typeof(System.Collections.Generic.Queue<byte>)));

            md.Body.Variables.Add(array);
            md.Body.Variables.Add(index);
            md.Body.Variables.Add(args);

            worker.Emit(OpCodes.Ldarg_0);
            worker.Emit(endOfArgs ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
            worker.Emit(OpCodes.Call, queueArgumentsMethod);
            worker.Emit(OpCodes.Stloc, args);

            foreach (char c in source.ToCharArray())
            {
                bool commented = inComment || inMultilineComment;
                if (beginComment != ' ')
                {
                    if (c == beginComment)
                    {
                        inComment = true;
                    }
                    else if (beginComment == '/' && c == '*')
                    {
                        inMultilineComment = true;
                    }
                    beginComment = ' ';
                }
                if (endMultilineComment)
                {
                    if (c == '/')
                    {
                        inMultilineComment = false;
                    }
                    endMultilineComment = false;
                }
                if ((c == '/' || c == ';') && !inComment && !inMultilineComment)
                {
                    beginComment = c;
                }
                else if (c == '*' && inMultilineComment)
                {
                    endMultilineComment = true;
                }
                else if (c == '\n' && inComment)
                {
                    inComment = false;
                }
                else if (c == '#' && !inComment && !inMultilineComment)
                {
                    inComment = true;
                }

                if ((commented || beginComment != ' ' || inComment || inMultilineComment) && !ignore)
                {
                    continue;
                }

                // if a program ends in +, -, > or < the opperation will not occure, but who will know?
                if (crementing && c != '+' && c != '-' && IsCommandChar(c))
                {
                    if (crementValue > 256 || crementValue < -256)
                    {
                        throw new CompilerException("Value overflow. Try compiling with --intArray.");
                    }
                    Crement(Math.Abs(crementValue), (crementValue < 0) ? false : true);
                    crementing = false;
                    crementValue = 0;
                }
                else if (moving && c != '>' && c != '<' && IsCommandChar(c))
                {
                    MovePointer(Math.Abs(moveValue), (moveValue < 0) ? false : true);
                    moving = false;
                    moveValue = 0;
                }
                switch (c)
                {
                    case '>':
                        if (cell == array_size)
                        {
                            array_size++;
                        }
                        cell++;
                        moving = true;
                        moveValue++;
                        break;
                    case '<':
                        cell--;
                        if (cell == array_start_offset)
                        {
                            array_size++;
                            array_start_offset--;
                        }
                        moving = true;
                        moveValue--;
                        break;
                    case '+':
                        crementValue++;
                        crementing = true;
                        break;
                    case '-':
                        crementValue--;
                        crementing = true;
                        break;
                    case '.':
                        {
                            worker.Emit(OpCodes.Ldloc, array);
                            worker.Emit(OpCodes.Ldloc, index);
                            worker.Emit(intArray ? OpCodes.Ldelem_I4 : OpCodes.Ldelem_I1);
                            worker.Emit(OpCodes.Call, Import(typeof(Console).GetMethod("Write", new Type[] { typeof(char) })));
                            break;
                        }
                    case ',':
                        {
                            worker.Emit(OpCodes.Ldloc, array);
                            worker.Emit(OpCodes.Ldloc, index);
                            worker.Emit(OpCodes.Ldloc, args);
                            worker.Emit(OpCodes.Call, readMethod);
                            worker.Emit(intArray ? OpCodes.Conv_I4 : OpCodes.Conv_U1);
                            worker.Emit(intArray ? OpCodes.Stelem_I4 : OpCodes.Stelem_I1);
                            break;
                        }
                    case '[':
                        {
                            loop++;
                            Loop l = new Loop();
                            l.LoopStart = worker.Create(OpCodes.Nop);
                            worker.Append(l.LoopStart);
                            loopList.Add(l);
                            break;
                        }
                    case ']':
                        {
                            loop--;
                            Loop l = null;
                            for (int i = loopList.Count - 1; i >= 0; i--)
                            {
                                if (loopList[i].ConditionStart == null)
                                {
                                    l = loopList[i];
                                    break;
                                }
                            }
                            if (l == null)
                            {
                                throw new CompilerException("Syntax Error: Unopened loop");
                            }

                            l.ConditionStart = worker.Create(OpCodes.Ldloc, array);
                            worker.Append(l.ConditionStart);
                            worker.Emit(OpCodes.Ldloc, index);
                            worker.Emit(intArray ? OpCodes.Ldelem_I4 : OpCodes.Ldelem_I1);
                            worker.Emit(OpCodes.Ldc_I4_0);
                            worker.Emit(OpCodes.Bgt, l.LoopStart);

                            Instruction check = worker.Create(OpCodes.Br, l.ConditionStart);
                            worker.InsertBefore(l.LoopStart, check);

                            break;
                        }
                    default:
                        if (c != ' ' && c != '\n' && c != '\t' && c != '@' && (int)c != 13 && !ignore)
                        {
                            throw new CompilerException("Syntax Error");
                        }
                        break;
                }
            }

            if (loop != 0)
            {
                throw new CompilerException("Syntax Error: Unclosed loop");
            }

            Instruction pushSize = worker.Create(OpCodes.Ldc_I4, array_size);
            Instruction createArray = worker.Create(OpCodes.Newarr, intArray ?
                Import(typeof(int)) : Import(typeof(byte)));
            Instruction storeArray = worker.Create(OpCodes.Stloc, array);
            Instruction loadOffset = worker.Create(OpCodes.Ldc_I4, Math.Abs(array_start_offset));
            Instruction storeOffset = worker.Create(OpCodes.Stloc, index);

            worker.InsertBefore(md.Body.Instructions[0], pushSize);
            worker.InsertAfter(pushSize, createArray);
            worker.InsertAfter(createArray, storeArray);
            worker.InsertAfter(storeArray, loadOffset);
            worker.InsertAfter(loadOffset, storeOffset);

            worker.Emit(OpCodes.Call, Import(typeof(Console).GetMethod("WriteLine", new Type[] { })));
            worker.Emit(OpCodes.Ret);
        }

        private static void MovePointer(int ammount, bool forward)
        {
            worker.Emit(OpCodes.Ldloc, index);
            worker.Emit(OpCodes.Ldc_I4, ammount);
            worker.Emit(forward ? OpCodes.Add : OpCodes.Sub);
            worker.Emit(OpCodes.Stloc, index);
        }

        private static void Crement(int ammount, bool increment)
        {
            worker.Emit(OpCodes.Ldloc, array);                             // load this
            worker.Emit(OpCodes.Ldloc, index);                             // and this for the stelem

            worker.Emit(OpCodes.Ldloc, array);                             // this
            worker.Emit(OpCodes.Ldloc, index);                             // and this get popped
            worker.Emit(intArray ? OpCodes.Ldelem_I4 : OpCodes.Ldelem_I1); // right now
            worker.Emit(OpCodes.Ldc_I4, ammount);
            worker.Emit(increment ? OpCodes.Add : OpCodes.Sub);
            worker.Emit(intArray ? OpCodes.Conv_I4 : OpCodes.Conv_U1);

            worker.Emit(intArray ? OpCodes.Stelem_I4 : OpCodes.Stelem_I1); // and here's that stelem
        }

        private static bool IsCommandChar(char c)
        {
            return (c == '>' || c == '<' || c == '+' || c == '-' || c == '.' || c == ',' || c == '[' || c == ']');
        }

        public static string Version
        {
            get { return "1.01"; }
        }
    }
    public class CompilerException : Exception
    {
        public CompilerException()
            : this("Unknown Compiler Exception")
        {
        }

        public CompilerException(string message)
            : base(message)
        {
        }
    }
    // These methods are injected into all new assemblies
    public static class InjectedMethods
    {
        public static Queue<int> QueueArguments(string[] args, bool endOfArgs)
        {
            Queue<int> queue = new Queue<int>();
            foreach (string arg in args)
            {
                bool gotFile = false;
                if (File.Exists(arg))
                {
                    try
                    {
                        foreach (char c in File.ReadAllText(arg).ToCharArray())
                        {
                            queue.Enqueue((int)c);
                        }
                        gotFile = true;
                    }
                    catch (IOException e)
                    {
                    }
                }
                if (!gotFile)
                {
                    foreach (char c in arg.ToCharArray())
                    {
                        queue.Enqueue((int)c);
                    }
                }
            }
            if (endOfArgs && queue.Count > 0)
            {
                queue.Enqueue(0);
            }
            return queue;
        }

        public static int Read(Queue<int> queue)
        {
            if (queue.Count > 0)
            {
                return queue.Dequeue();
            }
            else
            {
                return Console.Read();
            }
        }
    }

    // These methods are injected into new embedded interpreter assemblies
    public class InjectedInterpretMethods
    {
        public static void InterpretMain(string[] args, bool endOfArgs)
        {
            Queue<int> queue = InjectedMethods.QueueArguments(args, endOfArgs);

            System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
            Stream stream = assembly.GetManifestResourceStream("source");
            StreamReader sr = new StreamReader(stream);
            string source = sr.ReadToEnd();
            sr.Close();
            stream.Close();

            int size, offset;
            Interpreter.Analyze(source, out size, out offset);
            Interpreter interpreter = new Interpreter(size, offset);
            interpreter.Input = new InterpreterReader(queue);
            interpreter.Interpret(source);
        }
    }

    public class InterpreterReader : TextReader
    {
        private Queue<int> queue;

        public InterpreterReader(Queue<int> queue)
        {
            this.queue = queue;
        }

        public override int Read()
        {
            return InjectedMethods.Read(queue);
        }
    }
    public class Interpreter
    {
        public TextReader Input = Console.In;
        public TextWriter Output = Console.Out;

        private int[] array;
        private int index;
        private int array_size;

        public Interpreter()
            : this(30000, 0)
        {
        }

        public Interpreter(int array_size, int offset)
        {
            this.array_size = array_size;
            index = offset;
        }

        public static void Analyze(string source, out int size, out int offset)
        {
            size = 1;
            offset = 0;
            int cell = 1;
            foreach (char c in source.ToCharArray())
            {
                switch (c)
                {
                    case '>':
                        if (cell == size)
                        {
                            size++;
                        }
                        cell++;
                        break;
                    case '<':
                        cell--;
                        if (cell == offset)
                        {
                            size++;
                            offset--;
                        }
                        break;
                }
            }
            offset = Math.Abs(offset);
        }

        public void Interpret(string source)
        {
            array = new int[array_size];
            bool outputted = false;
            char last_char = '\n';
            char[] chars = source.ToCharArray();
            int i = 0;
            while (i < chars.Length)
            {
                switch (chars[i])
                {
                    case '>':
                        index++;
                        break;
                    case '<':
                        index--;
                        break;
                    case '+':
                        array[index]++;
                        break;
                    case '-':
                        array[index]--;
                        break;
                    case '.':
                        outputted = true;
                        last_char = (char)array[index];
                        Output.Write(last_char);
                        break;
                    case ',':
                        array[index] = Input.Read();
                        break;
                    case '[':
                        if (array[index] <= 0)
                        {
                            int loops = 1;
                            while (loops > 0)
                            {
                                i++;
                                if (chars[i] == ']')
                                {
                                    loops--;
                                }
                                else if (chars[i] == '[')
                                {
                                    loops++;
                                }
                            }
                        }
                        break;
                    case ']':
                        if (array[index] > 0)
                        {
                            int loops = 1;
                            while (loops > 0)
                            {
                                i--;
                                if (chars[i] == '[')
                                {
                                    loops--;
                                }
                                else if (chars[i] == ']')
                                {
                                    loops++;
                                }
                            }
                        }
                        break;
                }
                i++;
            }
            if (outputted && last_char != '\n' && last_char != '\r')
            {
                Output.WriteLine();
            }
        }
    }
    public class Source
    {
        private long array_size;
        public long ArraySize
        {
            get { return array_size; }
        }

        public Source(string source)
        {

        }
    }
}
